package app

import (
	"encoding/binary"
	"encoding/json"
	"sync"
	"time"

	"gitee.com/go-errors/errors"
	"gitee.com/go-wena/app/internal/bolt"
)

var Cache = &cacheService{}

type cacheService struct {
	db   *bolt.DB
	err  error
	once sync.Once
}

var cacheBucket = []byte("cache")

var ErrNotFound = errors.New("not found")

func (c *cacheService) init() {
	c.db, c.err = bolt.Open("cache.data", 0644, bolt.DefaultOptions)
}

func (c *cacheService) Get(bucket string, key string, out interface{}) error {
	if c.once.Do(c.init); c.err != nil {
		return c.err
	}
	var isExpired bool
	err := c.db.View(func(tx *bolt.Tx) error {
		bucket := tx.Bucket([]byte(bucket))
		if bucket == nil {
			return ErrNotFound
		}
		data := bucket.Get([]byte(key))
		if data == nil {
			return ErrNotFound
		}

		if len(data) > 8 {
			d := int64(binary.BigEndian.Uint64(data[:8]))
			if d > 0 && time.Now().UnixNano() > d {
				isExpired = true
				return ErrNotFound
			}
			data = data[8:]
		}

		return json.Unmarshal(data, out)
	})

	if err != nil {
		return err
	}

	if isExpired {
		_ = c.db.Update(func(tx *bolt.Tx) error {
			if bucket := tx.Bucket(cacheBucket); bucket != nil {
				return bucket.Delete([]byte(key))
			}
			return nil
		})
	}

	return nil
}

func (c *cacheService) Set(bucket string, key string, value interface{}, expireIn ...time.Duration) error {
	if c.once.Do(c.init); c.err != nil {
		return c.err
	}
	return c.db.Update(func(tx *bolt.Tx) error {
		bucket, err := tx.CreateBucketIfNotExists([]byte(bucket))
		if err != nil {
			return err
		}
		m, err := json.Marshal(value)
		if err != nil {
			return err
		}
		data := make([]byte, len(m)+8)
		copy(data[8:], m)
		if len(expireIn) > 0 && expireIn[0] > 0 {
			binary.BigEndian.PutUint64(data[:8],
				uint64(time.Now().Add(expireIn[0]).UnixNano()))
		}
		return bucket.Put([]byte(key), data)
	})
}

func (c *cacheService) NextSequence(bucket string) (seq uint64, err error) {
	err = c.db.Update(func(tx *bolt.Tx) error {
		bucket, err := tx.CreateBucketIfNotExists([]byte(bucket))
		if err != nil {
			return err
		}
		seq, err = bucket.NextSequence()
		return err
	})
	return
}

func (c *cacheService) Sequence(bucket string) (seq uint64, err error) {
	err = c.db.View(func(tx *bolt.Tx) error {
		if bucket := tx.Bucket([]byte(bucket)); bucket != nil {
			seq = bucket.Sequence()
		}
		return nil
	})
	return
}
